package cn.seaboot.admin.image;

import cn.seaboot.commons.exception.ServiceException;
import cn.seaboot.commons.lang.P;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.concurrent.NotThreadSafe;
import java.io.*;

/**
 * 执行CMD命令
 * <p>
 * {@link ProcessBuilder} 不能执行带空格的命令，整行命令会被认为是一个文件名；
 * {@link Runtime} 使用时，即便我们能保证命令是对的，但是很容易出现永久阻塞的情况，并且很难定位问题原因。
 * <p>
 * 这个工具，使用的仍然是{@link ProcessBuilder}，只是换了一种相对安全的使用方式，
 * 每一次执行 cmd 命令，都会构建一个不带参数的 bat 文件，通过 bat 文件调用 “需要执行的 cmd 命令”。
 * 简而言之，就是生成一个 bat 文件，通过 bat 调用 cmd 命令。
 * <p>
 * bat 文件以临时文件的方式生成，详见：{@link File#createTempFile(String, String)}，
 * 超高并发场合下，会出现同名的缓存文件（文件名有大十几位，和 GUID 一样，可以认为不会有并发问题），
 * 假设真的出现同名缓存文件，文件 "写入" 到 "执行" 的一瞬间，会出现幻读，通常可以忽略这个问题。
 *
 * @author Mr.css
 * @version 2022-02-15 9:38
 */
@NotThreadSafe
public class ProcessCommand {
    private static final Logger logger = LoggerFactory.getLogger(ProcessCommand.class);

    /**
     * 默认的编码，与系统环境相关
     */
    private static String defaultCharset = "GBK";


    /**
     * 设置默认编码
     *
     * @param encoding 编码
     */
    public static void shareCharset(String encoding) {
        logger.info("cmd default encoding has changed: {}", encoding);
        ProcessCommand.defaultCharset = encoding;
    }


    /**
     * fluent method
     *
     * @return -
     */
    public static ProcessCommand create(File workspace) throws IOException {
        return new ProcessCommand(workspace);
    }
    // static end --------------------------------------------------------------------------

    /**
     * 可执行命令文件
     */
    private final File workspace;
    /**
     * I/O编码，与命令执行过程无关，与执行结束的日志记录相关
     */
    private String charset = defaultCharset;
    /**
     * 需要运行的命令
     */
    private final StringBuilder cmd = new StringBuilder();

    /**
     * 生成一个{@link ProcessCommand}
     *
     * @param space 命令执行路径
     */
    private ProcessCommand(File space) throws IOException {
        this.workspace = space;
    }

    /**
     * 追加一段脚本
     *
     * @param str 脚本
     * @return this
     */
    public ProcessCommand append(String str) {
        cmd.append(str).append(" ");
        return this;
    }

    /**
     * 设置日志编码
     *
     * @param charset 编码
     * @return this
     */
    public ProcessCommand setCharset(String charset) {
        this.charset = charset;
        return this;
    }

    /**
     * 执行 cmd 命令，这个命令会阻塞线程，直到文件处理完毕。
     *
     * @return 执行结果码以及详细的日志信息
     * @throws IOException          bat临时文件生成失败，或者命令启动失败
     * @throws InterruptedException 等待执行结果时发生异常
     */
    public CommandResult waitForDetail() throws IOException, InterruptedException {
        // execute bat file
        Process result = this.run(true);

        // 已经启动了任务，需要进一步处理结果反馈。
        try (InputStream is = result.getInputStream();
             InputStreamReader bis = new InputStreamReader(is, this.charset);
             BufferedReader br = new BufferedReader(bis)) {
            String line;
            StringBuilder msg = new StringBuilder(120);
            while ((line = br.readLine()) != null) {
                msg.append(line).append(P.lineSeparator());
            }
            // thread will wait, until work has terminated
            int code = result.waitFor();
            return new CommandResult(code, msg.toString());
        }
    }

    /**
     * 执行CMD命令，这个命令会阻塞线程，直到文件处理完毕。
     *
     * @return Result
     * @throws IOException      bat临时文件生成失败，或者命令启动失败
     * @throws ServiceException 命令生成失败
     */
    public int waitFor() throws IOException, InterruptedException {
        return this.run(false).waitFor();
    }

    /**
     * 临时文件中写入命令
     *
     * @param redirectErrorStream 是否接受异常反馈
     * @return 执行结果
     * @throws IOException - 启动异常
     */
    private Process run(boolean redirectErrorStream) throws IOException {
        String cmd = this.cmd.toString();
        logger.debug("generate cmd: {}", cmd);

        // execute bat file
        ProcessBuilder processBuilder = new ProcessBuilder();
        processBuilder.command(cmd);
        processBuilder.redirectErrorStream(redirectErrorStream);
        return processBuilder.start();
    }
}
